package edu.drexel.jrexel2d.physics;

import java.util.*;

import edu.drexel.jrexel2d.collision.Collision;
import edu.drexel.jrexel2d.gamestate.*;
import edu.drexel.jrexel2d.input.CharacterActions;
import edu.drexel.jrexel2d.objects2D.*;
import edu.drexel.jrexel2d.physics.Common;
import java.util.concurrent.atomic.AtomicInteger;
import java.io.BufferedReader;
import java.io.FileReader;
import java.lang.Math;

public class PhysicsEngine {
	
	private Vector<FrictionCoefficient> kineticFrictionCoefficients;
	private Vector<FrictionCoefficient> staticFrictionCoefficients;
	private PhysicsState physicsEngineState;
	private AtomicInteger atomicInteger = new AtomicInteger();
	private Level level;
	private ArrayList<Object2D> refAll2DObjects;
	
	public final static int GRAVITATIONAL_ACCEL_CONSTANT = 20;
	
	public PhysicsEngine(){
		
		physicsEngineState = new PhysicsState();
		refAll2DObjects = new ArrayList<Object2D>();
		kineticFrictionCoefficients = new Vector<FrictionCoefficient>();
		staticFrictionCoefficients = new Vector<FrictionCoefficient> ();
	}
	
	public void init(Level inLevel){
		
		//"World" and "Level" are synonymous
		World2D newWorld = new World2D(Common.GRAVITATIONAL_ACCELERATION.EARTH, atomicInteger.incrementAndGet());
		int newWorldID = newWorld.getID();

		level = inLevel;
		refAll2DObjects = inLevel.getObjects();
		
		physicsEngineState.add2DWorld(newWorld);
		
		for(int i = 0; i < refAll2DObjects.size(); ++i){
			
			PhysicsObject2D new2DObject = new PhysicsObject2D(VelocityVector.ZERO_VEL_VECTOR, refAll2DObjects.get(i).getId(), refAll2DObjects, Math.abs(GRAVITATIONAL_ACCEL_CONSTANT * newWorld.getGravAcceleration()));
		    new2DObject.setWorldId(newWorldID);
		    
		    if(refAll2DObjects.get(i) instanceof Player){
		    	new2DObject.setIsPlayer(true);
		    }
		    
			physicsEngineState.add2DPhysicsObject(new2DObject);
		}
		
		loadFrictionCoefficients(Common.DEFAULT_FRICTION_CONSTS_FILE);
	}
	
	public void loadFrictionCoefficients(String configFile){
		
		try {
			BufferedReader fileBuffer = new BufferedReader(new FileReader("assets/physicsData/" + configFile));
			
			String line;

			while ((line = fileBuffer.readLine()) != null) {
				
				String token = line.trim();
				
				if (token.equals("static")){
					
					String staticLine;

					while (!((staticLine = fileBuffer.readLine()).trim()).equals("/static")) {
						String[] data = (staticLine.trim()).split(",");
						String object1 = data[0];
						String object2 = data[1];
						double value   = Double.valueOf(data[2]);
						
						staticFrictionCoefficients.add(new FrictionCoefficient(object1, object2, value));
					}					
				}
															
				if (token.equals("kinetic")){
						
					String kineticLine;

					while (!((kineticLine = fileBuffer.readLine()).trim()).equals("/kinetic")) {
						
						if(kineticLine != null || kineticLine != String.valueOf(" ")){
							String[] data = (kineticLine.trim()).split(",");
							
							String object1 = data[0];
							String object2 = data[1];
							double value   = Double.valueOf(data[2]);
							
							kineticFrictionCoefficients.add(new FrictionCoefficient(object1, object2, value));
						}
					}	
				}
			}

			fileBuffer.close();
		} catch (Exception e) {
			System.out.println("Error reading file: " + e);
		}
	}
	
	public void gameLoopUpdates(ArrayList<Collision> collisions, int delta){
		
		//Only process collisions if we have any
		if(collisions.size() > 0){

			processCollisions(collisions);
		}
		
		objectUpdates(delta);
	}
	
	private void processCollisions(ArrayList<Collision> inCollisions){
		
		for(int i = 0; i < inCollisions.size(); ++i){
			
			//No collisions with one's self
			if(inCollisions.get(i).getIds()[0] != inCollisions.get(i).getIds()[1]){
			  
			  //No collisions with picked objects	
			  if( !getObject2DById(inCollisions.get(i).getIds()[0]).isCurrentlyPicked() ||	!getObject2DById(inCollisions.get(i).getIds()[0]).isCurrentlyPicked()){
				  
				  try{
						 
						Object2D o1 = level.getObjectById(inCollisions.get(i).getIds()[0]);
						Object2D o2 = level.getObjectById(inCollisions.get(i).getIds()[1]);
						
						boolean pFlag = false ;
						if(o1 instanceof Player){
						
						  Player p1 = level.getPlayerByObjectId(o1.getId());
						  if(p1.getActions().size() == 0) {
							  // If the player isn't moving, push them out of the collision by default
							  p1.shift(0, -20) ;
							  pFlag = true ;
						  }
						  else {
							switch(p1.getActions().get(0)){
							case MOVE_UP:
								p1.shift(0, 20) ;
								break;
							case MOVE_DOWN:
								p1.shift(0, -20) ;
								break;
							case MOVE_LEFT:
								p1.shift(20, 0) ;
								break;
							case MOVE_RIGHT:
								p1.shift(-20, 0) ;
								break;
							default:
								break;
							}
						  }
						} //ENDIF o1 instanceof Player
						
						if(o1.isRolled()){
							physicsEngineState.getPhysicsObjectById(o1.getId()).getVelocity().setXAcceleration(physicsEngineState.getPhysicsObjectById(o1.getId()).getVelocity().getXAcceleration() * -1);
							physicsEngineState.getPhysicsObjectById(o1.getId()).getVelocity().setXVelocity(physicsEngineState.getPhysicsObjectById(o1.getId()).getVelocity().getXVelocity() * -1);
						}
						
						if(o2 instanceof Player){
							
							Player p2 = level.getPlayerByObjectId(o2.getId());
							  if(p2.getActions().size() == 0) {
								  if(!pFlag) {
									  // If the player isn't moving, push them out of the collision by default
									  p2.shift(0, -20) ;
								  }
							  }
							  else {
								switch(p2.getActions().get(0)){
								case MOVE_UP:
									p2.shift(0, 20) ;
									break;
								case MOVE_DOWN:
									p2.shift(0, -20) ;
									break;
								case MOVE_LEFT:
									p2.shift(20, 0) ;
									break;
								case MOVE_RIGHT:
									p2.shift(-20, 0) ;
									break;
								default:
									break;
								}
							  }
						} //ENDIF o2 instanceof Player	
						
					}//ENDTRY
					catch(Exception e){
						e.printStackTrace() ;
					} 
			  } //ENDIF check if colliding with a picked object
		  } //ENDIF check so object does not collide with itself	
		}//ENDFOR
	}
	
	private void objectUpdates(int delta){
	//NOTE: inDelta units = milliseconds
		
		for(PhysicsObject2D aPhysicsObject : physicsEngineState.getAll2DPhysicsObjects()){
						   
			//Updates for objects that are jumping
			if(aPhysicsObject.isJumping() || aPhysicsObject.isThrown() || aPhysicsObject.isRolled()){	
				
			  aPhysicsObject.update(delta);
				
			}//ENDIF Player is jumping or being thrown
			
		}//ENDFOR 
		
	}
	
	public PhysicsObject2D getPhysicsObjectById(int id){
		return physicsEngineState.getAll2DPhysicsObjects().elementAt(id);
	}
	
	public void add2DObject(PhysicsObject2D newObject){
		physicsEngineState.add2DPhysicsObject(newObject);
	}
	
	public void add2DWorld(World2D newWorld){
		physicsEngineState.add2DWorld(newWorld);
	}
	
	public void playerJump(int objectId){
		
		try{
			//only jump if not already jumping
			if(!physicsEngineState.getAll2DPhysicsObjects().elementAt(objectId).isJumping()){
			   
			   //set jumping vars for PhysicsObject2D
			   physicsEngineState.getAll2DPhysicsObjects().elementAt(objectId).jump(getObject2DById(objectId).getLocation());	   			   
			}
		}
		catch(ArrayIndexOutOfBoundsException e){
			System.out.println("That player index does not exist: " + e.toString()); 
		}
	}
	
	private Object2D getObject2DById(int inId){
		
		Object2D newObject = null;
		
		for(int i = 0; i < refAll2DObjects.size(); ++i){
			
			if(refAll2DObjects.get(i).getId() == inId){
				newObject = refAll2DObjects.get(i);
			}	
		}
		
		return newObject;
	}
	
	public void objectMove(int objId, int xOffset, int yOffset){
		
		//if object is jumping, then update its jumping point so it 
		//returns to the correct location if moved while jumping
		if(physicsEngineState.getAll2DPhysicsObjects().elementAt(objId).isJumping()){
			physicsEngineState.getAll2DPhysicsObjects().elementAt(objId).updateJumpPoint(xOffset, yOffset);
		}
	}
	
	public int getPickableObject(Point2D callerLocation, int callerObjId){
		
		int pickableObjectId = -1;
		
		float xMin = callerLocation.getXF() - 100;
		float xMax = callerLocation.getXF() + 100;
		float yMin = callerLocation.getYF() - 100;
		float yMax = callerLocation.getYF() + 100;
		
		for(Object2D anObject : refAll2DObjects){
			//If object is within the X range
			if( xMin <= anObject.getLocation().getXF() &&  anObject.getLocation().getXF() <= xMax){
				
				//If object is within the Y range
				if( yMin <= anObject.getLocation().getYF() &&  anObject.getLocation().getYF() <= yMax){
					
					//If the object is able to be picked up
					if(anObject.isPickable()){
						pickableObjectId = anObject.getId();
					}
				}//END IF check Y range
			}//END IF checking X range
		}//END FOR LOOP
		
		return pickableObjectId;
	}
 
	public void pickupObject(int callerObjId, int pickedUpObjId){
		
		Object2D refCaller = getObject2DById(callerObjId);
		Object2D refPickedUpObj = getObject2DById(pickedUpObjId);
		
		if(refCaller instanceof Player){
			((Player)refCaller).setHeldObjectId(pickedUpObjId);
		}
		
		refPickedUpObj.setCurrentlyPicked(true);
		refPickedUpObj.setLocation(refCaller.getLocation());	
	}
	
	public void shiftObject(int objId, float x, float y){
		this.getObject2DById(objId).shift(x, y);
	}
	
	public void throwObject(int callerObjId, int thrownObjId){
		
		Object2D refCaller = getObject2DById(callerObjId);
		Object2D refThrownObj = getObject2DById(thrownObjId);
		
		//Cut ties of having held the object
		refThrownObj.setCurrentlyPicked(false);
		
		//Create new location coordinates for thrown object 
		//since current location coordinates are pointing to the 
		//coordinates of the object holding it.
		refThrownObj.setLocation(new Point2D(refThrownObj.getLocation().getXF(), refThrownObj.getLocation().getYF()));
		
		if(refCaller instanceof Player){
			((Player)refCaller).setHeldObjectId(-1);
		}
		
		//Resolve throwing the object
		getPhysicsObjectById(thrownObjId).setThrown(true);
		
		//set ground location
		getPhysicsObjectById(thrownObjId).setThrownGroundLocation(new Point2D(0, getPhysicsObjectById(thrownObjId).getPosition().getYF() + 40));
		
		CharacterActions direction = getThrowDirection(refCaller);
		
		//set velocity of throw	within PhysicsObject class	
		getPhysicsObjectById(thrownObjId).throwObj(direction, GRAVITATIONAL_ACCEL_CONSTANT * physicsEngineState.getWorldById(getPhysicsObjectById(thrownObjId).getWorldId()).getGravAcceleration());
	}
	
	private CharacterActions getThrowDirection(Object2D throwerObject){
		
		CharacterActions directionToThrow = CharacterActions.MOVE_RIGHT;
		
		if(throwerObject instanceof Player){
			
			if(((Player) throwerObject).getLastAction() == CharacterActions.MOVE_LEFT){
				directionToThrow = CharacterActions.MOVE_LEFT;
			}
		} //END throwerObject instanceof Player
		
		return directionToThrow;
	}
	
	private CharacterActions getRollDirection(Object2D throwerObject){
	    return getThrowDirection(throwerObject);
	}
	
	public void rollObject(int callerObjId, int rolledObjId){
		
		Object2D refCaller = getObject2DById(callerObjId);
		Object2D refRolledObj = getObject2DById(rolledObjId);
		
		int xPositionOffset = 100;
		int yPositionOffset = 40;
		
		getPhysicsObjectById(rolledObjId).setRollDirection(getRollDirection(refCaller));
		
		CharacterActions direction = getPhysicsObjectById(rolledObjId).getRollDirection();
		
		if(direction == CharacterActions.MOVE_LEFT){
			xPositionOffset = xPositionOffset * -1;
		}
		else{
			xPositionOffset = Math.abs(xPositionOffset);
		}
		
		//Cut ties of having held the object
		refRolledObj.setCurrentlyPicked(false);
		
		//Set on ground with new location coordinates for rolled object 
		//since current location coordinates are pointing to the 
		//coordinates of the object holding it.
		refRolledObj.setLocation(new Point2D(refRolledObj.getLocation().getXF() + xPositionOffset, refRolledObj.getLocation().getYF() + yPositionOffset));
		
		if(refCaller instanceof Player){
			((Player)refCaller).setHeldObjectId(-1);
		}
		
		
		//Resolve throwing the object
		getPhysicsObjectById(rolledObjId).setRolled(true);
		
		getPhysicsObjectById(rolledObjId).setCurrentStaticFrictionCoeff(getStaticFrictionCoeff("smiley", "ground", direction));
		
		//set velocity of roll	within PhysicsObject class	
		getPhysicsObjectById(rolledObjId).rollObj(direction);
		
	}
	
	private double getStaticFrictionCoeff(String obj1, String obj2, CharacterActions direction){
	   
		double coeff = 0;
		boolean done = false;
		
		for(int c = 0; c <= staticFrictionCoefficients.size() && !done; ++c){
			
			if(staticFrictionCoefficients.get(c).getKey1().equals(obj1)){
				if(staticFrictionCoefficients.get(c).getKey2().equals(obj2)){
					coeff = staticFrictionCoefficients.get(c).getValue();
					done = true;
				}
			}
			else if(staticFrictionCoefficients.get(c).getKey1().equals(obj2)){
			   if(staticFrictionCoefficients.get(c).getKey2().equals(obj1)){
				   coeff = staticFrictionCoefficients.get(c).getValue();
				   done = true;
			   }
			}
		}
		
		if(coeff != 0){
			
			switch(direction){
			   
			case MOVE_LEFT:
				 coeff = Math.abs(coeff);
				 break;
			case MOVE_RIGHT:
				 coeff = Math.abs(coeff) * -1;
				 break;
			case MOVE_UP:
				 coeff = Math.abs(coeff);
				 break;
			case MOVE_DOWN:
				coeff = Math.abs(coeff) * -1;
				break;
			default:
				 break;
			}
			
		}
		
		return coeff;
		
	}
}

